package eiisan.config.security.auth;

import eiisan.config.security.settings.SecurityProperties;
import eiisan.config.security.util.Matcher;
import eiisan.util.common.EmptyUtil;
import eiisan.util.spring.ContextUtil;
import org.springframework.context.annotation.DependsOn;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 权限匹配管理器
 * 参考org.springframework.security.access.vote.AffirmativeBased配置
 * 参考org.springframework.security.access.vote.RoleVoter配置
 * @author gragonfly
 * @date 2019/7/7
 **/
@Component
@DependsOn("contextUtil")
public class AuthorityManager implements AccessDecisionManager {
    //默认权限
    private ConfigAttribute defaultDeniedRole;

    @PostConstruct
    private void init() {
        SecurityProperties properties = ContextUtil.get(SecurityProperties.class);
        defaultDeniedRole = new SecurityConfig(properties.getAuth().getDeniedRole());
        Assert.notNull(defaultDeniedRole, "defaultDeniedRole must not be null");
    }

    /**
     * 更新权限信息
     */
    public void refresh() {
        synchronized (this) {
            defaultDeniedRole = null;
            init();
        }
    }

    /**
     * 判定是否拥有权限<br>
     * authentication是UserDetailsServiceImpl中添加到GrantedAuthority中的权限信息.<br>
     * object包含客户端请求的request信息，可转换为HttpServletRequest,方法如下:<br>
     * request = ((FilterInvocation) object).getHttpRequest()<br>
     * attributes是DatabaseSecurityMetadataSource的getAttributes方法的返回值.<br>
     * 如果用户不具有请求的url的权限,抛出AccessDeniedException.<br>
     */
    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> permissions)
            throws AccessDeniedException, InsufficientAuthenticationException {
        if (!Matcher.needVerify(((FilterInvocation) object).getHttpRequest().getRequestURI())) {
            // 如果是免验证路径,则直接放行
            return;
        }
        if (EmptyUtil.yes(auth.getAuthorities())) {
            throw new AccessDeniedException("无访问权限!");
        }
        // 当包含无访问权限时,直接驳回(此时只有无访问权限一个权限)
        if (permissions.contains(defaultDeniedRole)) {
            throw new AccessDeniedException("无访问权限!");
        }
        Set<String> auths = auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors
                .toSet());
        for (ConfigAttribute permission : permissions) {
            if (auths.contains(permission.getAttribute())) {
                return;
            }
        }
       // 当token携带权限与资源所需访问权限不符时,驳回
        throw new AccessDeniedException("无访问权限!");
    }

    //权限attribute 是否支持
    @Override
    public boolean supports(ConfigAttribute attribute) {
        /* 参考org.springframework.security.access.vote.AbstractAccessDecisionManager
           调用投票者Voter，使用decisionVoters类的supports
          例如controller方法上使用@PreAuthorize("hasAuthority('read')")@PreAuthorize("hasRole('role_admin')")等注解
           Voter.supports方法用前缀来判断属性是否支持
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().startsWith("ROLE_")) {
            return true;
        }
        else {
            return false;
        }*/



//=========================================
        /*
        * 自定义的eiisan.config.security.auth.AuthoritySource.getAttributes
        * 返回的Collection<ConfigAttribute>会被这里supports判断，
        * （allCache里的每一个ConfigAttribute）
        * 是否能够处理使用传递
        * 直接true
        * */
        return true;
    }

    /**
     * This implementation supports any type of class, because it does not query the
     * presented secure object.
     *
     * @param clazz the secure object
     *
     * @return always <code>true</code>
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}
